Skip to content

fix(metrics): detect key collisions between dimensions, metrics, and metadata#5240

Open
Zelys-DFKH wants to merge 7 commits into
aws-powertools:mainfrom
Zelys-DFKH:fix/metrics-key-collision-detection
Open

fix(metrics): detect key collisions between dimensions, metrics, and metadata#5240
Zelys-DFKH wants to merge 7 commits into
aws-powertools:mainfrom
Zelys-DFKH:fix/metrics-key-collision-detection

Conversation

@Zelys-DFKH
Copy link
Copy Markdown
Contributor

@Zelys-DFKH Zelys-DFKH commented May 11, 2026

Summary

Closes #5208. `serializeMetrics()` builds EMF output by spreading multiple key sources in a fixed order: metadata → defaultDimensions → dimensions → dimensionSets → metricValues. When a key appears in more than one source, the later value wins silently. Two of those overwrites break the EMF schema:

  • A metric name matching a string key (dimension or metadata) replaces the string value with a number, so CloudWatch drops the dimension or fails to parse the metric.
  • A string key matching a metric name replaces a number with a string, causing the same failure in the opposite call order.

The bug is call-order-dependent. A guard at the call site of `addMetric()` or `addDimension()` can only catch one direction; the fix belongs at `serializeMetrics()`, where all sources are present before the spread.

What changed

  • `Metrics.ts`: Added `#checkKeyCollisions()`, called from `serializeMetrics()` before the spread. Seeds string keys in spread-precedence order (metadata lowest, dimension set highest), warns when a key appears in two string sources, throws when a metric name matches any string key.
  • `types/Metrics.ts` and `types/index.ts`: Added `EmfKeySource` union type for source labels in error and warning messages.
  • `dimensions.test.ts`: Eight new tests: five for metric/dimension name collisions (regular dimension, default dimension, built-in `service` dimension, via `addDimensions`, and reverse call order) and three for dimension-on-dimension overwrites that should warn.
  • `metadata.test.ts`: Six new tests: two for metadata/metric name collisions in both call orderings and four for metadata-on-dimension overwrites that should warn.

Issue number: Closes #5208


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Disclaimer: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful.

@svozza
Copy link
Copy Markdown
Contributor

svozza commented May 17, 2026

Thanks for tackling this! Two pieces of feedback:

1. Only throw on type changes

Same-type collisions (dimension ↔ metadata, both strings) overwrite silently but don't corrupt the EMF schema — CloudWatch only rejects when a key flips between string and number. Throwing on same-type clashes is stricter than the spec requires and inconsistent with addDimension, which already handles dimension-on-dimension overwrites with a warn. I'd keep the throw for type changes only and leave same-type collisions on the existing warn path.

2. Centralise the check in serializeMetrics()

The current guards in storeMetric() and addMetadata() only catch two of the four type-change directions. They miss:

  • addDimension/addDimensions/setDefaultDimensions after addMetric (num → str)
  • addMetric after addMetadata (str → num)

Reverse the call order in either of the existing tests and the corruption comes back. Rather than adding guards to every mutator, serializeMetrics() already has all the sources in scope right before the spread that causes the overwrite — one pass there catches every combination regardless of call order:

// inside serializeMetrics(), just before the return
type Source = 'dimension' | 'default dimension' | 'dimension set' | 'metadata';
const stringKeys = new Map<string, Source>();
for (const k of Object.keys(defaultDimensions)) stringKeys.set(k, 'default dimension');
for (const k of Object.keys(dimensions)) stringKeys.set(k, 'dimension');
for (const set of dimensionSets) for (const k of Object.keys(set)) stringKeys.set(k, 'dimension set');
for (const k of Object.keys(this.#metadataStore.getAll())) stringKeys.set(k, 'metadata');

for (const name of Object.keys(metricValues)) {
  const source = stringKeys.get(name);
  if (source) {
    throw new Error(
      `EMF key collision on "${name}": registered as both a metric (number) and a ${source} (string)`
    );
  }
}

Per reviewer feedback: move the type-change collision guard from individual
mutators (storeMetric, addMetadata) to serializeMetrics(), where all string
and numeric sources are in scope simultaneously.

The prior approach only caught two of the four collision directions
(metric-after-dimension and metadata-after-metric). The centralized check
catches all four regardless of call order, and throws only on type-change
collisions (metric number vs. string source) not same-type overwrites, which
CloudWatch handles silently.

New error format: 'EMF key collision on "<key>": registered as both a metric
(number) and a <source> (string)' where source is 'dimension', 'default
dimension', 'dimension set', or 'metadata'.

Tests updated to assert at serializeMetrics() time; two reverse-direction
tests added (dimension-after-metric, metadata-before-metric).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@svozza
Copy link
Copy Markdown
Contributor

svozza commented May 21, 2026

Hi @Zelys-DFKH, do you need any help with this PR? Anything you need me to clarify from my comment?

@powertools-for-aws-oss-automation powertools-for-aws-oss-automation Bot removed the size/M PR between 30-99 LOC label May 21, 2026
@Zelys-DFKH
Copy link
Copy Markdown
Contributor Author

Sorry for the slow response, I missed the notification. Both points were right.

Pushed a second commit that removes the guards from storeMetric() and addMetadata() and moves the check into serializeMetrics(), where all four sources (default dimensions, dimensions, dimension sets, metadata) are in scope before the spread. The throw is scoped to number-vs-string only, so same-type string overwrites stay on the existing warn path. Tests cover both call orderings for the metadata case and the reverse-order dimension case.

Thanks for the ping.

@powertools-for-aws-oss-automation powertools-for-aws-oss-automation Bot added the size/L PRs between 100-499 LOC label May 21, 2026
@svozza
Copy link
Copy Markdown
Contributor

svozza commented May 21, 2026

No worries! Couple of comments, the way the code is now, we only warn on dimesnions overwriting each other but we should warn whenever any keys are overwritten. Secondly, there are merge conflicts in the test file that need to be resolved. Also, the serializeMetrics method has become quite long, you can see there is now a warning about the cyclomatic complexity so we may want to pull this new validation logic into a private method.

…ision check

- Pull collision detection out of serializeMetrics() into a private
  #checkKeyCollisions() method, addressing the cyclomatic complexity
  warning flagged by SonarCloud
- Extend the check to warn (via the logger) when a metadata key would
  silently overwrite a dimension, default dimension, or dimension set
  value in the serialized EMF output; metric collisions still throw
- Add three regression tests in metadata.test.ts covering the new
  metadata-vs-dimension-source warning path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@powertools-for-aws-oss-automation powertools-for-aws-oss-automation Bot added size/L PRs between 100-499 LOC and removed size/L PRs between 100-499 LOC labels May 22, 2026
Comment thread packages/metrics/src/Metrics.ts Outdated
metricValues: Record<string, number | number[]>,
metadata: Record<string, string>
): void {
type Source =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not inline types, there's a types module for this.

Comment thread packages/metrics/src/Metrics.ts
…pstream

Extends #checkKeyCollisions to warn whenever any two string sources share a
key (dimension overwriting default dimension, dimension set overwriting
dimension or default dimension), not just when metadata overlaps with a
dimension. Folds the repeated get+check+set pattern into a single setKey
helper.

Also reconciles dimensions.test.ts with upstream aws-powertools#5229 (MAX_DIMENSION_COUNT
loop bounds, toThrow vs toThrowError, three new upstream test cases) and adds
regression tests for the three new cross-source overwrite warn paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@powertools-for-aws-oss-automation powertools-for-aws-oss-automation Bot added size/L PRs between 100-499 LOC and removed size/L PRs between 100-499 LOC labels May 22, 2026
@Zelys-DFKH
Copy link
Copy Markdown
Contributor Author

Zelys-DFKH commented May 22, 2026

Thanks for the thorough review — pushed b2a61fa.

  1. setKey now warns whenever any source overwrites another (dimension over default dimension, dimension set over dimension or default dimension), not just when metadata overlaps. The metric throw is unchanged.

  2. Synced dimensions.test.ts with upstream's fix(metrics): enforce dimension limits per-array #5229 (loop bounds, toThrow vs toThrowError, the three new upstream tests) and added regression tests for the new warn paths.

  3. The repeated get/check/set blocks are now a single setKey helper, which absorbed warnOverwrite. serializeMetrics is unchanged. The complexity was all in #checkKeyCollisions, which is now shorter.

Let me know if anything else needs a look!

@svozza
Copy link
Copy Markdown
Contributor

svozza commented May 23, 2026

Left some comments, also looks like we still have merge conflicts

…over metadata

Three changes addressing PR review feedback:

1. EmfKeySource extracted from the inline type alias in #checkKeyCollisions
   to packages/metrics/src/types/Metrics.ts.

2. Dimensions now take precedence over metadata in the serialized EMF. The
   prior spread order put metadata last, so a metadata value silently
   overrode any dimension on the same key, contradicting the warning that
   said the dimension would win. The fix flips the spread (metadata first,
   then dimensions) and reorders the seed in #checkKeyCollisions so the
   warning message matches what actually ends up in the payload.

3. Collision tests moved from dimensions.test.ts to a new
   keyCollisions.test.ts. dimensions.test.ts is now byte-identical to
   upstream/main, which clears the merge conflict GitHub flagged after aws-powertools#5229
   merged. Also drops the now-unused DimensionsStore.getDimensionCount(),
   which aws-powertools#5229 stopped calling.

Test count: 156 unit tests pass.
@powertools-for-aws-oss-automation powertools-for-aws-oss-automation Bot added size/L PRs between 100-499 LOC and removed size/L PRs between 100-499 LOC labels May 23, 2026
@powertools-for-aws-oss-automation powertools-for-aws-oss-automation Bot added size/L PRs between 100-499 LOC and removed size/L PRs between 100-499 LOC labels May 23, 2026
@Zelys-DFKH
Copy link
Copy Markdown
Contributor Author

Thanks for the careful review @svozza. Pushed 3e92ae8 with all three points addressed.

  1. Moved EmfKeySource into packages/metrics/src/types/Metrics.ts, re-exported from types/index.ts and imported in Metrics.ts.

  2. Dimensions take precedence over metadata. While I was in there, I noticed the spread order in serializeMetrics had metadata last, which was silently overriding dimensions in the output even though the warning said the dimension would win. Flipped the spread (metadata first, dimensions last) and reordered the seed in #checkKeyCollisions to match. Warning message and payload now agree.

  3. Cleared the merge conflict by moving my collision tests into a new packages/metrics/tests/unit/keyCollisions.test.ts. dimensions.test.ts is now byte-identical to upstream/main, so GitHub's 3-way merge resolves cleanly. Also dropped DimensionsStore.getDimensionCount() since fix(metrics): enforce dimension limits per-array #5229 stopped using it.

All checks green.

import { Metrics, MetricUnit } from '../../src/index.js';

describe('EMF key collision detection', () => {
const ENVIRONMENT_VARIABLES = process.env;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use the built in Vitest environment variable functions here: https://vitest.dev/api/vi.html#vi-stubenv.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted keyCollisions.test.ts. The 8 tests are now in dimensions.test.ts, which uses the same env setup.

@@ -0,0 +1,138 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I missing something, these tests look incredibile similar to the ones in metadata.test.ts?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests weren't duplicating metadata.test.ts (those cover metadata/metric collisions, these cover dimension/metric), but having a separate file for it was the real issue. Moved all 8 into dimensions.test.ts and deleted keyCollisions.test.ts. Dimension/metric and dimension/dimension collision tests live there now; metadata.test.ts owns the metadata side.

Deleted keyCollisions.test.ts and moved all 8 tests into
dimensions.test.ts, where they belong alongside the existing
dimension behavior tests. metadata.test.ts owns the metadata-side
collision tests; dimensions.test.ts now owns the dimension-side.

Co-authored-by: Claude
Signed-off-by: Zelys-DFKH <admin@coracreacrafts.com>
@powertools-for-aws-oss-automation powertools-for-aws-oss-automation Bot removed the size/L PRs between 100-499 LOC label May 24, 2026
@powertools-for-aws-oss-automation powertools-for-aws-oss-automation Bot added the size/L PRs between 100-499 LOC label May 24, 2026
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L PRs between 100-499 LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: No collision detection between dimension keys, metric names, and metadata keys in EMF output

2 participants